<?php


require_once dirname(__FILE__).'/PEAR/Net/Socket.php';
require_once dirname(__FILE__).'/SDOSocketConfig.php';
require_once(dirname(__FILE__).'/../uc/libraries/UCLibLogger.php');
require_once dirname(__FILE__).'/../statistics/Statistic.inc.php';

/**
 * SDO 专用socket 底层操作
 * 根据协议进行打包，解包操作
 * @category   SDO
 * @package    Relation
 * @author     ggg <wangweijun01@snda.com> 2010-3-15 17:23
 * @copyright  SDO OP
 * @license    http://www.opensource.org/licenses/bsd-license.php  BSD License
 * @link       http://www.sdo.com/
 * @since      File available since Release 1.0.0
 */

class SDOSocket {

    
    const EC_SOCKET_CREATE      = -1;
    const EC_SOCKET_CONNECT     = -2;
    const EC_SOCKET_SELECT      = -3;
    const EC_SOCKET_TIMEOUT     = -4;
    const EC_SOCKET_READ        = -5;
    const EC_SOCKET_WRITE       = -6;
    const EC_SOCKET_PACK        = -7;
    const EC_SOCKET_HEADER      = -8;
    
   
    static protected $errorMap = array(
        self::EC_SOCKET_CREATE      => "Socket Create Error",  
        self::EC_SOCKET_CONNECT     => "Socket Connect Error",
        self::EC_SOCKET_SELECT      => "Socket Select Error",
        self::EC_SOCKET_TIMEOUT     => "Socket Timeout",
        self::EC_SOCKET_READ        => "Socket Recv Error",
        self::EC_SOCKET_WRITE       => "Socket Send Error",
        self::EC_SOCKET_PACK        => "Socket Pack Error",
        self::EC_SOCKET_HEADER      => "Socket Header Error"
    );
    
	
 	/**
	 * 采用http方式请求的url
	 */
	 private static $_httpRestUrl	=  'http://114.80.135.7:8080/';
	 
 	/**
	 * 对要发送的数据进行打包
	 * @param String apiName API名称
	 * @param String apiParam API相关参数
	 * @return  String 返回打包好后的数据
	 */
	protected function pack($apiName,$apiParam)
	{
	    
	    if(!is_array($apiParam))
        {
            $apiParam   = array();
        }
        $apiParam   = http_build_query($apiParam);
		

		//2字节整型，协议版本号，目前版本号1
		$uVersion	= 1;
		//2字节整型，协议格式    0x0000  json格式协议 0x0001 xml格式协议
        $uProtocolType  = 0x0000;;
        //4字节整型， 命令 0x01000001 心跳包 0x01000002 心跳应答 0x02000001 请求服务 0x02000002 服务应答
        $uCmd   = 0x02000001;
        //4字节整型， 应用端标识（盛大SDO统一分配的ID）
        $uClientID  = 0x01000001;
        //4字节整型， 应用端发起的事务ID
        $uClientTransID = 0x01000001;
        //8字节整型， 应用端发起请求的用户ID，服务器回填
        $uClientUserIDLow  = 0x01000001;
        $uClientUserIDHigh  = 0x01000001;

        $pack_head  = pack('nnNNNNN',$uVersion,$uProtocolType,$uCmd,$uClientID,$uClientTransID,
                            $uClientUserIDLow,$uClientUserIDHigh);

		//-------------- 包体 ---
		$pack_body	= '';
        
        //4字节整型，需要调用的API名称字符串字节长度
        $uAPINameLength = (0x00000000 | strlen($apiName));
		$pack_body.= pack("N",$uAPINameLength);

		//uAPINameLength 长度的API名称字符串
		$strAPIName	= $apiName;
		$pack_body.=$strAPIName;

		//4字节整型，API对应的参数列表串字节长度。
		$uParamListLength	= (0x00000000 | strlen($apiParam));
		$pack_body.= pack("N",$uParamListLength);

		//uParamListLength长度的参数列表串
		$strParamList	= $apiParam;
		$pack_body.=$strParamList;
	   
		//拼上总包长度,返回结果
		$uSize = 0x00000000;
		$tmp_pack_str	= $pack_head.$pack_body;
		$ret_str	= pack("N",(0x00000000 | (strlen($tmp_pack_str) + 4))).$tmp_pack_str;
		return $ret_str;
	}

	/*
	 * 解包数据头部
	 * @param $header 数据头部
	 */
    protected function headerUnpack($header)
    {
        $unpackedArray = unpack("NuSize/nuVersion/nuProtocolType/".
                                "NuCmd/NuClientID/NuClientTransIDD/NuClientUserIDLow/".
                                "NuClientUserIDHigh",
                                  $header);
                                  
        return $unpackedArray;
    }
    
    /*
     * 得到头部长度
     * @param array $unpackedHeader
     * @return int
     */
    public function getHeaderLength()
    {
        return 28;
    }
    
    /*
     * 得到数据长度
     * @param array $unpackedHeader
     * @return int
     */
    protected function getBodyLength($unpackedHeader)
    {
        return $unpackedHeader['uSize'] - 28;
    }
	
 	/**
	 * 对收到的的数据进行解包
	 * @param String 
	 * @return String
	 */
	protected function unPack($param)
	{
	    $unpackedArray = unpack('NuAnswerLength/a*Answer',$param);
        return json_decode($unpackedArray['Answer'], true);
	}

 	/**
	 * http 方式请求
	 * @param String $apiName API名称
	 * @param Array $apiParam API相关参数
	 * @return  
	 */
	public function httpRequest($apiName,$apiParam='')
	{

//print_r($apiParam);					
		if(!is_array($apiParam))
			$apiParam	= array();
		$tmp_param	= http_build_query($apiParam);
	
		//转成按 / 分割
		$apiName	= strtr($apiName, '.', "/");
		$tmp_url	= self::$_httpRestUrl . $apiName.'?'.$tmp_param;
//echo $tmp_url;		
		//需要返回json格式
		$option	= array(CURLOPT_HTTPHEADER => Array('application/json;charset=UTF-8'));
		$result_str	= self::curlRequest($tmp_url,'',$option);
		return json_decode($result_str,true);

	}


 	/**
	 * curl 方式请求
	 * @param String $url 请求网址
	 * @param String $postString 要post的参数 格式如  xxx=1&yyy=2
	 * @param Array $option 参数 如 array(CURLOPT_REFERER=>'http://www.51.com/',CURLOPT_HTTPHEADER=>array('Host: client.sdo.com'))
	 *  
	 * @return  
	 */
	public function curlRequest($url,$postString='',$option='')
	{
		$ch = curl_init();
		curl_setopt($ch,CURLOPT_URL,$url);
		curl_setopt($ch,CURLOPT_POSTFIELDS,$postString);
		curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
		//curl_setopt($ch,CURLOPT_USERAGENT,$_SERVER['HTTP_USER_AGENT']);

		//设置其他参数
		if(!empty($option) && is_array($option))
		foreach($option as $key => $val)
		{
			curl_setopt($ch, $key,$val);
		}

		$data = curl_exec($ch);
		//$info = curl_getinfo($ch);
		curl_close($ch);
		return $data;
	}

 	/**
	 * 发送数据包
	 * @param String apiName API名称
	 * @param Array $apiParam API相关参数
	 * @return  
	 */
	static public function send($apiName,$apiParam)
	{
        $socket = new SDOSocket();
        return $socket->_send($apiName,$apiParam);
	}
	
	
	public function _send($apiName,$apiParam)
	{
		$_time_start = microtime(true);
	    //数据打包
        $pack_str   = $this->pack($apiName,$apiParam);

        //发送数据包
		$config = SDOSocketConfig::getConfig($apiName);
        $result_str = $this->tpcRequest($pack_str, $config);

        if (!is_string($result_str))
        {
            $errorCode = $result_str;
            UCLibLogger::rpc("$apiName Error #".json_encode($apiParam)." (code:400)", microtime(true) - $_time_start, $config['host'], $config['port'], UCLIB_LOG_RPC_TYPE_UNIFIED);
            return array(
              'code' => 400,
              'msg' => implode(':', array($errorCode." ", " ".self::$errorMap[$errorCode]))
            );
        }
        else
        {  
            //数据解包，返回
            $result = $this->unPack($result_str);
            
            $errno = (is_array($result) && array_key_exists('code', $result))? $result['code']:-1;
            $error = (is_array($result) && array_key_exists('msg', $result))? $result['msg']:'';
            if(! (is_array($result) && $result['status']==200 && $result['code'] == 0) ){ 
                UCLibLogger::rpc("$apiName Error #".json_encode($apiParam)." (code:$errno,msg:$error,return:".json_encode($result).")", microtime(true) - $_time_start, $config['host'], $config['port'], UCLIB_LOG_RPC_TYPE_UNIFIED);
            } else {
                UCLibLogger::rpc($apiName.' Ok #'.json_encode($apiParam), microtime(true) - $_time_start, $config['host'], $config['port'], UCLIB_LOG_RPC_TYPE_UNIFIED);
            }
            return $result; 
        }
	}
	
	/*
	 * 检查返回是否正常
	 * @param $result 调用send后的结果
	 * @return boolean
	 */
	static function isError($result)
	{
	    if (isset($result['code']))
	    {
			
			//孙宁 [11:00]:
			//code=0都是成功的
			//如果是错误的，code会是相应的错误码，有错误描述的异常会有msg字段
            if ($result['code'] == 0)
            {
	           return false;
            }
	    }

		//这里可以进行记录错误的监控
		StatUserinfo('api_error', $result['code']);

	    return true;
	}

	
	/**
	 * @param String apiName API名称
	 * @param String apiParam API相关参数
	 * @param Boolen isNoBlock 是否要阻塞方式
	 * @param Boolen isRead 是否要接收服务器返回
	 * 发起 tcp 请求
	 * @return  
	 * isRead 
	 */
	protected function tpcRequest($comm,$serverInfo,$isNoBlock=false,$isRead=true)
	{
	    
	    $sock = new Net_Socket();
	    
        // open connection
        $result = $sock->connect($serverInfo['host'], $serverInfo['port'], false, 0.5);
        
        if($sock->isError($result))
        {
            return self::EC_SOCKET_CONNECT;
        }
       
        $sock->setTimeout(5,0);
        
        
        $len = strlen($comm);
        $hasWrite = 0;
        while ($hasWrite != $len)
        {
            $result = $sock->write($comm);
            if($sock->isError($result))
            {
                $sock->disconnect();
                return self::EC_SOCKET_WRITE;
            }
            
            $comm = strstr($comm, $result);
            $hasWrite += $result;
        }
        
		
	    
		//不理服务器返回
		if (!$isRead)
		{
			socket_close($sock);
			return "";
		}
		
	    //获取应答包长度
        $header = $sock->read($this->getHeaderLength());
        if($sock->isError($header) || empty($header))
        {
            $sock->disconnect();
            return self::EC_SOCKET_READ;
        }
        
        
        
        $header = $this->headerUnpack($header);
        if (false === $header)
        {
            $sock->disconnect();
            return self::EC_SOCKET_HEADER;
        }
        
        $len = $this->getBodyLength($header);
        
        
        //获取应答包体
        $Body = "";
        $hasRead = 0;
        while ($hasRead != $len)
        {
            $data = $sock->read($len - $hasRead);
            if($sock->isError($data) || empty($data))
            {
                $sock->disconnect();
                return self::EC_SOCKET_READ;
            }
            
            $hasRead += strlen($data);
            $Body .= $data;
        }
		
		$sock->disconnect();

		
        return $Body;
	}

}

//$result = SDOSocket::send("PeopleService.getPeople",array('sdid'=>1));
//print_r($result);
